/*
 * Galaxium Messenger
 * 
 * Copyright (C) 2009 Philippe Durand <draekz@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Collections.Generic;
using System.Reflection;

using Anculus.Core;
using Galaxium.Core;
using Galaxium.Client;
using Galaxium.Protocol;

namespace Galaxium.Protocol.Irc
{
	public class DccChatEventArgs : EventArgs
	{
		private string _message;
		public string Message { get { return _message; } }
		
		public DccChatEventArgs (string message)
		{
			_message = message;
		}
	}
	
	//http://en.wikipedia.org/wiki/Direct_Client-to-Client
	//http://www.irchelp.org/irchelp/rfc/ctcpspec.html
	public class DccChatConnection : TCPConnection
	{
		public event EventHandler<DccChatEventArgs> MessageReceived;
		public event EventHandler<DccChatEventArgs> ActionReceived;
		
		private IrcConversation _conversation;

		private bool _listenMode;
		private Thread _listenThread;
		private Socket _listenSocket;
		private Socket _clientSocket;
		private List<byte> _received = new List<byte> ();
		
		public override bool IsConnected
		{
			get
			{
				if (_listenMode && _clientSocket != null)
				{
					return _clientSocket.Connected;
				}
				else
					return base.IsConnected;
			}
		}
		
		public DccChatConnection (IrcSession session, IrcConversation conversation, DccConnectionInfo connectionInfo)
			: base (session, connectionInfo)
		{
			this._conversation = conversation;
			this._listenMode = !connectionInfo.IsInbound;
		}

		public override void Connect ()
		{			
			if (_listenMode)
			{
				_intendedDisconnect = false;
				
				try
				{
					IPEndPoint listenEndPoint = null;
					_listenSocket = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
					
					int tries = 5;
					bool success = false;
					
					// Attempt to open ports using upnp
					int externalPort = 0;
					
					NetworkUtility.CreatePortMap (ConnectionInfo.Port, out externalPort);
					
					while (--tries >= 0)
					{
						try
						{
							listenEndPoint = new IPEndPoint (NetworkUtility.LocalIPAddress, ConnectionInfo.Port);
							_listenSocket.Bind (listenEndPoint);
							success = true;
							break;
						}
						catch (Exception e)
						{
							Log.Error (e, "Exception while trying to bind to address/port.");
						}
					}
					
					if (success)
					{
						_listenSocket.Listen (1);
						
						_listenThread = new Thread (new ThreadStart (ListenSocketStart));
						_listenThread.Priority = ThreadPriority.Lowest;
						_listenThread.IsBackground = true;
						_listenThread.Start ();
					}
					else
					{
						Log.Debug ("Failed to setup a listening socket!");
						OnErrorOccurred (new ConnectionErrorEventArgs (this, "PROTOCOL_LISTEN", "Unable to listen for connections."));
					}
				}
				catch (SocketException ex)
				{
					Log.Error ("EndConnectCallback", ex);
					OnErrorOccurred (new ConnectionErrorEventArgs (this, "PROTOCOL_CONNECT", "Unable to connect to server."));
				}
			}
			else
			{
				// Not sure if this is needed here... but it seemed to help during testing with other clients that dont use upnp
				int externalPort = 0;
				if (!NetworkUtility.CreatePortMap (ConnectionInfo.Port, out externalPort))
				{
					Log.Debug ("Unable to map port!");
				}
				
				base.Connect ();
			}
		}
		
		public override void Disconnect ()
		{
			if (_listenMode)
			{
				if (_listenThread != null)
				{
					try
					{
						_listenThread.Abort ();
					}
					catch (ThreadAbortException)
					{
						
					}
					finally
					{
						_listenThread = null;
					}
				}
				
				if (_listenSocket != null && _listenSocket.Connected)
				{
					_listenSocket.Disconnect (false);
					_listenSocket.Close ();
				}
				
				if (_clientSocket != null && _clientSocket.Connected)
				{
					_clientSocket.Disconnect (false);
					_clientSocket.Close();
				}
				
				OnClosed (new ConnectionEventArgs (this));
			}
			else
			{
				base.Disconnect ();
			}
			
			NetworkUtility.DeletePortMap (ConnectionInfo.Port);
		}
		
		private void ListenSocketStart ()
		{
			try
			{
				_clientSocket = _listenSocket.Accept ();
				
				//TODO: if reverse mode, we need to read the data that the client socket sends
				
				OnEstablished (new ConnectionEventArgs (this));
				OnAfterConnect (new ConnectionEventArgs (this));
				
				while (_clientSocket.Connected)
				{
					byte[] buffer = new byte[8096];
					int rec = _clientSocket.Receive (buffer, SocketFlags.None);
					
					if (rec == 0)
					{
						_clientSocket.Close ();
						break;
					}
					
					ProcessIncomingBytes (buffer, rec);
				}
				
				OnClosed (new ConnectionEventArgs (this));
			}
			catch (SocketException ex)
			{
				Log.Error (ex, "Exception during ListenSocketStart thread!");
				OnErrorOccurred (new ConnectionErrorEventArgs (this, "PROTOCOL_LISTEN", "Error while accepting socket."));
			}
		}
		
		protected override void Dispose (bool disposing)
		{
			if (!_disposed)
			{
				if (disposing)
				{
					if (_listenThread != null)
					{
						try
						{
							_listenThread.Abort ();
						}
						catch (ThreadAbortException)
						{
							
						}
						finally
						{
							_listenThread = null;
						}
					}
				}
			}

			base.Dispose (disposing);
		}
		
		protected override void OnDataReceived (ConnectionDataEventArgs args)
		{
			ProcessIncomingBytes (args.Buffer, args.Length);
		}
		
		private void ProcessIncomingBytes (byte[] bytes, int length)
		{
			for (int i = 0; i < length; i++)
			{
				// Keep looking for 0x0A and if we get it, end the line.
				if (bytes[i] == 0x00A)
				{
					string line = Encoding.UTF8.GetString (_received.ToArray());
					_received.Clear();
					
					if (line[0] == (char)0x001)
					{
						string[] act = line.Split ((char)0x001);
						
						if (act.Length > 0)
						{
							foreach (string a in act)
								if (a.IndexOf ("ACTION") >= 0)
									OnActionReceived (a.Substring (7));
						}
					}
					else
					{
						if (!String.IsNullOrEmpty(line))
							ThreadUtility.Dispatch (() => OnMessageReceived (line));
					}
				}
				else
					_received.Add (bytes[i]);
			}
		}
		
		public void SendPrivateMessage (string message)
		{
			if (_listenMode)
				_clientSocket.Send (Encoding.UTF8.GetBytes (message+Environment.NewLine));
			else
				Send (Encoding.UTF8.GetBytes (message+Environment.NewLine));
		}
		
		protected override void OnClosed (ConnectionEventArgs args)
		{
			//NetworkUtility.DeletePortMap (ConnectionInfo.Port);
			
			base.OnClosed (args);
		}

		internal void OnMessageReceived (string message)
		{
			if (MessageReceived != null)
				ThreadUtility.Dispatch (() => MessageReceived (this, new DccChatEventArgs (message)));
		}
		
		internal void OnActionReceived (string message)
		{
			if (ActionReceived != null)
				ThreadUtility.Dispatch (() => ActionReceived (this, new DccChatEventArgs (message)));
		}
	}
}